<?php

/**
 * Description of Form
 *
 * @author chaporgin_a
 */
class VA_Form_Form {
    /**
     *
     * @var string
     */
    private $title = null;
    /**
     * array of VA_Form_Form_Element
     *
     * @var array
     */
    private $elements = array();
    /**
     * Сообщение от новой формы
     * @var string
     */
    private $newFormMessage = null;
    /**
     * Сообщение о том, что был превышен таймаут и что надо обновить страницу
     * @var string
     */
    private $timeoutMessage = null;

    /**
     * Сообщение о возврате формы
     * @var string
     */
    private $returnMessage = null;
    /**
     * Сообщение от запощенной формы
     * @var string
     */
    private $postedFormMessage = null;
    /**
     * Default timeout in minutes
     *
     * @var int
     */
    private $timeout = 1;
    /**
     * Ассоциативный массив сообщений от полей формы о результатах валидации
     * @var array
     */
    private $fieldMessages = array();
    /**
     * Через сколько форму можно отправлять снова
     * @var int
     */
    private $postLimit = null;

    /**
     *
     * @var string
     */
    private $postLimitMessage = null;

    private $timeoutToken = null;
    /**
     * Сообщения от самой формы
     *
     * @var array
     */
    private $messages = array();
    /**
     * Элементам формы уже были присвоены некоторые значения
     * @var boolean
     */
    private $isBinded = FALSE;

    private $remoteCheckLink = FALSE;

    private $gid = null;

    static public $debug = FALSE;

    public function setGID($gid) {
        if( null === $this->gid) {
            $this->gid = $gid;
        }
        return $this;
    }


    private function getGID() {
        return $this->gid;
    }
    public function getPrefix() {
        return 'jform_'.$this->getGID();
    }
    const PUT_ID = 0;
    const PUT_NAME = 1;
    /**
     * Помещает элемент в пространство имен формы
     *
     * "Подсказывает" элементу как формировать свой ID и NAME атрибуты
     * @param VA_Form_FormElement $element
     * @param const $putType
     * @return string
     */
    public function putIntoNamespace(VA_Form_FormElement $element, $putType) {
        if( $putType === self::PUT_ID) {
            return $this->getPrefix().'_'.$element->getName();
        } elseif(self::PUT_NAME === $putType) {
            return $this->getPrefix().'['.$element->getName().']';
        }
    }
    /**
     * Геттер для совсем уж простых скалярных величин
     *
     * @param <type> $name
     * @return <type>
     */
    public function  __get($name) {
        $allowed = array('timeout', 'title', 'elements');
        if(in_array($name, $allowed)) {
            switch ($name) {
                default:
                    return $this->$name;
            }
        } else {
            throw new Exception('tried to access inaccessible property '.$name);
        }
    }
    public function   __set($name,  $value) {
        $allowed = array('timeout', 'title');
        if(in_array($name, $allowed)) {
            switch ($name) {
                default:
                    $this->$name = $value;
                    return $this;
            }
        } else {
            throw new Exception('tried to access inaccessible property '.$name);
        }
    }
    /**
     *
     * @param VA_Form_FormElement $element
     * @return VA_Form_Form
     */
    public function addElement(VA_Form_FormElement $element) {
        $this->elements[] = $element;
        $element->setForm($this);
        return $this;
    }
    /**
     *
     * @return array
     */
    public function getElements() {
        return $this->elements;
    }
    /**
     * Proxy to mapper
     * @param string $xml
     * @return VA_Form_Form
     */
    static public function fromXML($xml) {
        $mapper = new VA_Form_Form_Mapper_XML();
        return $mapper->FromSource($xml);
    }

    /**
     * Proxy to serialize form
     *
     * @param VA_Form_Form $form form to serialize
     * @return string XML
     */
    static public function toXML(VA_Form_Form $form) {

        return '';
    }
    /**
     *
     * @param string $title
     * @return VA_Form_Form
     */
    public function setTitle($title) {
        $this->title = $title;
        return $this;
    }

    public function getTitle() {
        return $this->title;
    }
    /**
     *
     * @param integer $timeout
     * @return VA_Form_Form
     */
    public function setTimeout($timeout) {
        $this->timeout = $timeout;
        return $this;
    }

    /**
     *
     * @return integer
     */
    public function getTimeout() {
        return $this->timeout;
    }

    /**
     *
     * @param string $message
     * @return VA_Form_Form
     */
    public function setTimeoutMessage($message) {
        $this->timeoutMessage = $message;
        return $this;
    }
    /**
     *
     * @return string
     */
    public function getTimeoutMessage() {
        return $this->timeoutMessage;
    }

    public function setReturnMessage($message) {
        $this->returnMessage = $message;
        return $this;
    }

    public function getReturnMessage() {
        return $this->returnMessage;
    }

    public function setPostedFormMessage($message) {
        $this->postedFormMessage = $message;
        return $this;
    }

    public function getPostedFormMessage() {
        return $this->postedFormMessage;
    }
    /**
     *
     * @param string $message
     * @return VA_Form_Form
     */
    public function setNewFormMessage($message) {
        if(!empty ($message)) $this->newFormMessage = $message;
        return $this;
    }

    public function getNewFormMessage() {
        return $this->newFormMessage;
    }

    public function  __toString() {
        // тут на всякий случай сгенерируем токен - если не надо, то ничего не отвалится :)
        $this->generateTimeoutToken();
        $return = '';
        $return .= $this->renderMessages();
        /* @var $element VA_Form_FormElement */
        foreach($this->getElements() as $element) {
            $return .= $element->render();
        }
        // decorate form with ajax loader
        return '<div class="jformWrapper"><img class="ajaxLoading" alt="load indicator" />'.$return.'</div>';
    }
    /**
     * Show form's own messages
     * @return string
     */
    public function renderMessages() {
        $return = '';
        if(!empty ($this->messages)) {
            $return .= '<ul class="errorMessagesWrapper">';
            foreach($this->messages as $message) {
                $return .= sprintf('<li class="error">%s</li>', $message);
            }
            $return .= '</ul>';
        }
        return $return;
    }
    /**
     * Pass here array of values from user (e.g. $_POST)
     * @param array $params
     * @param array $files массив с информацией о загруженных файлах
     */
    public function bind($params, $files = array()) {
        /*var_dump($files);
        exit;*/
        $this->isBinded = TRUE;
        $elements = $this->getElementsAssoc();
        /* @var $e VA_Form_FormElement */
        foreach($elements as $e) $e->ResetValue();
        foreach($params as $name => $value) {
            if($name == 'token') $this->setTimeoutToken($value);
            elseif(key_exists($name, $elements) && !($elements[$name] instanceof VA_Form_FormElement_File)) {
                if( is_array($value)) {
                    foreach($value as $vPart) $elements[$name]->setValue($vPart);
                } else $elements[$name]->setValue($value);
            }
        }
        $this->init($params);
        //TODO: убрать этот хак с кодировкой
        $enc = mb_internal_encoding();
        mb_internal_encoding('UTF-8');
        foreach($files as $name => $value) {
            if(array_key_exists($name, $elements) && $elements[$name] instanceof VA_Form_FormElement_File) {
                foreach($files[$name] as $uploadedFile) {
                    $file = new VA_Form_File_UploadedFile($uploadedFile);
                    if($file->isValid()) {
                        $pointer = $elements[$name]->getStorage()->Put($file);
                        $publicId = $this->rememberUploadedFile($pointer, $elements[$name]);
                        $this->debug(sprintf('uploaded file %s', $pointer), 'bind');
                        $v = new VA_Form_File_TemporaryFile(
                                $elements[$name]->getStorage()
                                , $pointer
                                , $publicId
                        );
                        $elements[$name]->addValue($v);
                    }
                }
            }
        }
        mb_internal_encoding($enc);
    }
    /**
     * To collect data from outer sources, at present, the source is database
     */
    private function init($params) {
        /* @var $db JDatabaseMySQL */
        $db = JFactory::getDBO();
        $elements = $this->getElementsAssoc();
        $omitPublicIds = array("''");
        /* @var $e VA_Form_FormElement_File */
        foreach ($elements as $name => $e) {
            $cond = ($e instanceof VA_Form_FormElement_File) && isset($params[$name]['cancel_upload']) && (is_array($params[$name]['cancel_upload']));
            if($cond)
                foreach($params[$name]['cancel_upload'] as $id)
                    if(!empty($id)) $omitPublicIds[] = "'".$id."'";
        }
        $query  = 'DELETE FROM tmp_files_vu WHERE `public_id` IN (%s)';
        $query = sprintf($query, join(',', $omitPublicIds));
        $db->setQuery($query);
        $db->query();
        $query  = 'SELECT `filename`, `field`, `public_id`, `file_id` FROM tmp_files_vu WHERE token = %s';
        $query = sprintf($query
                , $db->Quote($this->getTimeoutToken())
        );
        $db->setQuery($query);
        $db->query();
        $result = $db->loadAssocList();
        if(!empty ($result)) {

            foreach($result as $row) {
                $name = $row['field'];
                if(key_exists($name, $elements) && $elements[$name] instanceof VA_Form_FormElement_File) {
                    $this->debug(
                            sprintf('remembered file %s, `file_id`=%s',$row['filename'], $row['file_id']),
                            'init'
                    );
                    $elements[$row['field']]->addValue(new VA_Form_File_TemporaryFile(
                            $elements[$row['field']]->getStorage()
                            , $row['filename']
                            , $row['public_id']
                    ));
                }
            }
        }
    }
    /**
     * Stores saved files in database. Uses Joomla API
     *
     * @param string $filePointer
     * @return string публичный ID который можно использовать для пользователей
     */
    private function rememberUploadedFile($filePointer, VA_Form_FormElement_File $element) {
        /* @var $db JDatabaseMySQL */
        $db = JFactory::getDBO();
        $query = 'INSERT INTO #__jforms_tmp_files (filename, field, token, created_at) VALUES (%s, %s, %s, NOW())';
        $query = sprintf($query
                , $db->Quote($filePointer)
                , $db->Quote($element->getName())
                , $db->Quote($this->getTimeoutToken())
        );
        $db->setQuery($query);
        $db->query();
        $this->debug(sprintf('added toked %s for file %s',$this->getTimeoutToken(),$filePointer), 'rememberUploadedFile');
        return md5($filePointer.$element->getName());
    }
    static public function debug($mes, $cont) {
        if(self::$debug) {
            echo sprintf('<p><strong>%s</strong> %s</p>',$cont, $mes);
        }
    }

    /**
     * Transforms $_FILES array, provided by PHP into more convinient one
     * массив для метода bind
     * @param array $files
     * @return array
     */
    static public function prepareFiles($files) {
        $return = array();
        if( is_array($files))
            foreach($files['name'] as $name => $value) {
                self::debug(sprintf('got file %s, is_array? = %d', $name, is_array($value)), 'prepareFiles');
                if(!is_array($value))
                    $return[$name] = array(
                            array(
                                    'name' => $value
                                    , 'type' => $files['type'][$name]
                                    , 'tmp_name' => $files['tmp_name'][$name]
                                    , 'error' => $files['error'][$name]
                                    , 'size' => $files['size'][$name]
                    ));
                else {
                    $count = count($value);
                    $r = array();
                    for($i=0;$i<$count;++$i)
                        $r[] = array('name' => $value[$i]
                                , 'type' => $files['type'][$name][$i]
                                , 'tmp_name' => $files['tmp_name'][$name][$i]
                                , 'error' => $files['error'][$name][$i]
                                , 'size' => $files['size'][$name][$i]
                        );
                    $return[$name] = $r;
                }
            }
        return $return;
    }
    /**
     * To use in bind method to comfortably bind values to elements
     * @return array
     */
    private function getElementsAssoc() {
        $return = array();
        /* @var $element VA_Form_FormElement */
        foreach($this->getElements() as $element) {
            //NOTICE: I do not return HtmlBlocks - no need to modify it ever
            if($element instanceof VA_Form_FormElement_HtmlBlock) continue;
            $return[$element->getName()] = $element;
        }
        return $return;
    }
    /**
     * To get current values of form elements
     *
     * @return array
     */
    public function getAssocValues() {
        $return = array();
        /* @var $element VA_Form_FormElement */
        foreach($this->getElements() as $element)
            $return[$element->getName()] = $element->getValue();
        return $return;
    }

    /**
     * Use in ajax requests.
     *
     * Those elements without values are not validated here
     * @return boolean
     */
    public function isPartialValid() {
        //TODO: убрать этот хак с кодировкой
        $enc = mb_internal_encoding();
        mb_internal_encoding('UTF-8');
        if(!$this->isBinded) return TRUE;
        $return = TRUE;
        $elements = $this->getElementsAssoc();
        /* @var $element VA_Form_FormElement */
        foreach($elements as $name => $element)
            if($element->hasValue()) {
                if($element->isValid()) $this->fieldMessages[$name] = $element->getErrorMessages();
                else $return = FALSE;
            }
        mb_internal_encoding($enc);
        return $return;
    }

    /**
     *
     * @return boolean
     */
    public function isValid() {
        //TODO: remove this encoding hack (to save cyrillics after passing it to validator)
        $enc = mb_internal_encoding();
        mb_internal_encoding('UTF-8');
        if(!$this->isBinded) return TRUE;
        $return = TRUE;
        if( $this->checkTimeoutToken() === FALSE) {
            $this->addFormMessage($this->timeoutMessage);
            $return = FALSE;
        }
        if( $this->checkPostLimit() === FALSE) {
            $this->addFormMessage($this->getPostLimitMessage());
            $return = FALSE;
        }
        $elements = $this->getElementsAssoc();
        /* @var $element VA_Form_FormElement */
        foreach($elements as $name => $element)
            if($element->isValid()) $this->fieldMessages[$name] = $element->getErrorMessages();
            else $return = FALSE;
        if($return === FALSE)
            $this->addFormMessage($this->returnMessage);
        mb_internal_encoding($enc);
        return $return;
    }

    /**
     * Use during validation to question DB
     * @return boolean
     */
    public function checkPostLimit() {
        $db = JFactory::getDBO();
        $query = <<<QUERY
  SELECT COUNT(*) FROM #__jforms_posted
    WHERE form_name=%s
    AND ip=INET_ATON(%s)
    AND last_posted + INTERVAL %d MINUTE > NOW()
QUERY;
        $query = sprintf($query
                , $db->Quote($this->getGID())
                , $db->Quote($_SERVER['REMOTE_ADDR'])
                , $this->getPostLimit()
        );
        $db->setQuery($query);
        $db->query();
        $result = $db->loadResultArray();
        if( $result[0] == 0 ) return TRUE;
        return FALSE;
    }

    /**
     * Event handler to find out value of postLimit
     *
     * Must be called explicitly - to be more compatible with other frameworks
     */
    public function postEvent() {
        /* @var $db JDatabaseMySQL */
        $db = JFactory::getDBO();

        /**
         * сохраняем файлы
         * TODO: этот код дублируется
         */
        $values = array();
        $el = $this->getElementsAssoc();
        /*if(isset($_FILES[$this->getPrefix()])) {
            foreach(VA_Form_Form::prepareFiles($_FILES[$this->getPrefix()]) as $name => $value) {
                if(key_exists($name, $el)){
                    $values[] = $name.'='.$db->Quote($el[$name]->getValue());
                }
            }
            $query = sprintf('UPDATE #__jforms_%s SET %s WHERE id=%d'
                    , $this->getGID()
                    , join(', ', $values)
                    , $db->insertid()
            );
            $db->setQuery($query);
            $db->query();
        }*/
        $query = 'DELETE FROM #__jforms_posted WHERE form_name=%s AND ip=INET_ATON(%s)';
        $query = sprintf($query
                , $db->Quote($this->getGID())
                , $db->Quote($_SERVER['REMOTE_ADDR'])
        );
        $db->setQuery($query);
        $db->query();
        $query = 'INSERT INTO #__jforms_posted (form_name, ip) VALUES (%s, INET_ATON(%s))';
        $query = sprintf($query
                , $db->Quote($this->getGID())
                , $db->Quote($_SERVER['REMOTE_ADDR'])
        );
        $db->setQuery($query);
        $db->query();
    }

    /**
     * To assign token to the form
     *
     * Token is neeeded to check, that data was sent during NORMAL (=safe) flow,
     * and not during a robot-attack
     *
     * It must be called explicitly
     */
    private function handleToken() {
        $element = new VA_Form_FormElement_Hidden();
        $element->setValue($this->timeoutToken)
                ->setName('token');
        $this->addElement($element);
    }
    private function generateTimeoutToken() {
        if( null === $this->timeoutToken) {
            /* @var $db JDatabaseMySQL */
            $db = JFactory::getDBO();
            $token = md5(date('Y-M-D H:m:i').'a72jdc0');
            $query = sprintf('DELETE FROM #__jforms_forms WHERE token="%s"', $token);
            $db->setQuery($query);
            $db->query();
            $query = 'DELETE FROM #__jforms_token WHERE created_at + INTERVAL 1 DAY < NOW()';
            $db->setQuery($query);
            $db->query();
            $query = sprintf('INSERT INTO #__jforms_token (token) VALUES ("%s")', $token);
            $db->setQuery($query);
            $db->query();
            $this->timeoutToken = $token;
            $this->handleToken();
        }
        return $this->timeoutToken;
    }
    /**
     * Must be set only once - when form is created by a mapper for examplte
     */
    public function setTimeoutToken($token) {
        if( null === $this->timeoutToken) {
            $this->timeoutToken = $token;
            $this->handleToken();
        }
        $this->debug(sprintf('my token is %s', $this->timeoutToken), 'setTimeoutToken');
        return $this;
    }
    public function getTimeoutToken() {

        return $this->timeoutToken;
    }
    public function checkTimeoutToken() {
        if( null !== $this->getTimeoutToken()) {
            if($this->getTimeoutToken() == 0) return TRUE;
            /* @var $db JDatabaseMySQL */
            $db = JFactory::getDBO();
            $query = <<<QUERY
  SELECT *
    FROM #__jforms_token
    WHERE created_at + INTERVAL %d MINUTE > NOW()
    AND token = "%s"
QUERY;
            $query = sprintf($query
                    , $this->timeout
                    , $this->getTimeoutToken()
            );
            $db->setQuery($query);
            $db->query();
            return $db->getNumRows()>0?TRUE:FALSE;
        }
        throw new Exception('token invalid');
    }
    public function addFormMessage($message) {
        $this->messages[] = $message;
        return $this;
    }

    public function setRemoteCheckLink($link) {
        $this->remoteCheckLink = $link;
        return $this;
    }

    public function getRemoteCheckLink() {
        return $this->remoteCheckLink;
    }

    /**
     * Not finished! To get error messages
     *
     * It now returns messages from elements, ignoring messages from form
     *
     * @return array
     */
    public function getErrors() {
        return $this->fieldMessages;
    }

    /**
     *
     * @param int $limit
     */
    public function setPostLimit($limit) {
        $this->postLimit = $limit;
        return $this;
    }

    public function getPostLimit() {
        return $this->postLimit;
    }

    /**
     *
     * @param string $message
     * @return VA_Form_Form
     */
    public function setPostLimitMessage($message) {
        $this->postLimitMessage = $message;
        return $this;
    }

    public function getPostLimitMessage() {
        return $this->postLimitMessage;
    }

}

